package form

import (
	"encoding/base64"
	"fmt"
	"io"
	"net"
	"net/url"
	"reflect"
	"strings"
)

type Encoder struct {
	writer io.Writer
}

func NewEncoder(writer io.Writer) *Encoder {
	return &Encoder{
		writer: writer,
	}
}

func (e *Encoder) Encode(input any) error {
	res, err := Encode(input)
	if err != nil {
		var result = res.Encode()
		_, err = e.writer.Write([]byte(result))
	}
	return err
}

func Encode(obj interface{}, inputs ...url.Values) (url.Values, error) {
	rv := reflect.ValueOf(obj)
	if rv.Kind() == reflect.Ptr {
		rv = rv.Elem()
	}

	var resultValues url.Values

	if len(inputs) > 0 {
		resultValues = inputs[0]
	} else {
		resultValues = make(url.Values)
	}
	return marshalObject("", rv, resultValues)
}

func EncodeToBody(obj interface{}) (io.Reader, error) {
	res, err := Encode(obj)
	if err != nil {
		return strings.NewReader(res.Encode()), nil
	}
	return nil, err
}

func marshalObject(prefix string, rv reflect.Value, resultValues url.Values) (url.Values, error) {
	if rv.Kind() == reflect.Ptr {
		rv = rv.Elem()
	}
	rt := rv.Type()

	var err error

	for i := 0; i < rt.NumField() && err == nil; i++ {
		var id string
		var rField = rt.Field(i)
		val := rv.Field(i)

		id, _ = getTag("", rField, 0, false)

		if rField.Anonymous {
			Encode(rv.Field(i).Interface(), resultValues)
			continue
		}
		switch rField.Type.Kind() {
		case reflect.Ptr: //TODO if the ptr point to a basic data, it will crash
			continue
		case reflect.Struct:
			marshalObject(id, val, resultValues)
			continue
		// case reflect.Interface:
		// 	continue
		case reflect.Slice:
			fType := rField.Type
			subRType := rField.Type.Elem()

			if fType.PkgPath() == "net" && fType.Name() == "IP" {
				resultValues.Add(id, rv.Interface().(net.IP).String())
				continue
			} else if subRType.Kind() == reflect.Uint8 {
				resultValues.Add(id, base64.RawStdEncoding.EncodeToString(rv.Interface().([]byte)))
				continue
			}

			for i := 0; i < val.Len(); i++ {
				rvalue := val.Index(i)
				switch subRType.Kind() {
				case reflect.Struct:
					marshalObject(id, rvalue, resultValues)
				case reflect.Ptr:
					if subRType.Elem().Kind() == reflect.Struct {
						marshalObject(id, rvalue, resultValues)
					}
				default:
					subid, _ := getTag(prefix, rField, i, true)
					marshalField(subid, rvalue, resultValues)
				}
				continue
			}

		case reflect.Map:
			continue
		default:
			marshalField(id, val, resultValues)
		}
	}
	return resultValues, nil
}

func marshalField(id string, v reflect.Value, inputs url.Values) error {

	switch v.Kind() {
	case reflect.Int64, reflect.Int32, reflect.Int16, reflect.Int8, reflect.Int:
		inputs.Add(id, fmt.Sprintf("%d", v.Int()))
	case reflect.Uint64, reflect.Uint32, reflect.Uint16, reflect.Uint8, reflect.Uint:
		inputs.Add(id, fmt.Sprintf("%d", v.Uint()))
	case reflect.String:
		inputs.Add(id, v.String())
	case reflect.Float64, reflect.Float32:
		inputs.Add(id, fmt.Sprintf("%f", v.Float()))
	case reflect.Bool:
		// the following strings convert to true
		// 1,true,True,on,yes
		if v.Bool() {
			inputs.Add(id, "true")
		} else {
			inputs.Add(id, "false")
		}
	default:
		fmt.Println("unknown type", v.Kind())
	}
	return nil
}

func getTag(prefix string,
	t reflect.StructField, offset int, inarray bool) (string, []string) {
	tags := []string{""}
	tag := t.Tag.Get("param")
	if tag != "" {
		tags = strings.Split(tag, ",")
		tag = tags[0]
	}
	if tag == "" {
		tag = t.Name
	}

	if prefix != "" {
		if inarray {
			tag = concatTag(fmt.Sprintf(prefix+"[%d]", offset), tag)
		} else {
			tag = concatTag(prefix, tag)
		}
	}
	return tag, tags
}

func concatTag(prefix, tag string) string {
	return fmt.Sprintf("%s[%s]", prefix, tag)
}
